
namespace Stee.CAP8.DBAccess
{
    using Microsoft.EntityFrameworkCore;
    using System;
    using System.Threading.Tasks;
    using Microsoft.Extensions.Configuration;
    using Stee.CAP8.Entity;
    using System.Linq;
    using Microsoft.Extensions.Logging;
    using NLog.Extensions.Logging;
    using System.Threading;
    using NLog;

    public class BaseDBContext : DbContext, IUnitOfWork
    {
        private string connectionString;
        private bool useOracle;
        private readonly static Logger Logger = LogManager.GetCurrentClassLogger();
        internal static readonly ILoggerFactory NlogLoggerFactory
            = LoggerFactory.Create(builder => {
                builder
                .AddFilter((category, level) =>
                    category == DbLoggerCategory.Database.Command.Name
                    && (level == Microsoft.Extensions.Logging.LogLevel.Warning || level == Microsoft.Extensions.Logging.LogLevel.Error ||  level == Microsoft.Extensions.Logging.LogLevel.Critical))
                .AddNLog("nlog.config"); });
        /// <summary>
        /// The constructor will retrieve connection string from the program's appsettings.json and set it into TContext's connection.
        /// <para>setting <paramref name="useOracle"/> to true will use Oracle as database. </para>
        /// <para>setting <paramref name="useOracle"/> to true will use SQL as database. </para>
        /// </summary>
        public BaseDBContext(bool useOracle = true): this(null,useOracle) { }
        /// <summary>
        /// If the DbContextOption's database connection is configured, it will use what is already configured.
        /// <para>Else, it will set appsettings.json's "DefaultConnection" as the database connection string and connect to Oracle database. </para>
        /// </summary>
        public BaseDBContext(DbContextOptions options) : base(options)
        {
            if (string.IsNullOrEmpty(this.connectionString))
            {
                SetDefaultConnectionString(fromDI: true);
            }
        }
        /// <summary>
        /// The constructor gets the connection string from its first parameter and set it into TContext's connection.
        /// <para>setting <paramref name="useOracle"/> to true will use Oracle as database. </para>
        /// <para>setting <paramref name="useOracle"/> to true will use SQL as database. </para>
        /// </summary>
        public BaseDBContext(string conStr, bool usingOracle = true)
        {
            if (string.IsNullOrEmpty(conStr))
            {
                SetDefaultConnectionString(usingOracle);
            }
            else
            {
                this.connectionString = conStr;
                this.useOracle = usingOracle;
            }
        }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (optionsBuilder != null)
            {
                optionsBuilder.UseLoggerFactory(NlogLoggerFactory);
                // check if dependency injection has configured
                if (!optionsBuilder.IsConfigured)
                {
                    if (useOracle)
                    {
                        optionsBuilder.UseOracle(connectionString);
                    }
                    else
                    {
                        optionsBuilder.UseSqlServer(connectionString);
                    }
                }
            }
        }
        /// <summary>
        /// Synchronously, saves all changes made in the UnitOfWork's DbContext to the database.
        /// </summary>
        public virtual new int SaveChanges()
        {
            UpdateEntitiesOnAddOrUpdate();
            Logger.Trace("Saving changes to database.");
            return base.SaveChanges();
        }

        private void UpdateEntitiesOnAddOrUpdate()
        {
            foreach (var entity in this.ChangeTracker.Entries().Where(e => e.State == EntityState.Modified || e.State == EntityState.Added))
            {
                var saveEntity = entity.Entity as BaseEntity;
                saveEntity.LastUpdate = DateTime.Now;

                if (entity.State == EntityState.Added)
                {
                    saveEntity.Version = 0;
                }
                else if (entity.State == EntityState.Modified)
                {
                    saveEntity.Version += 1;
                }
            }
        }
        /// <summary>
        /// Asynchronously, saves all changes made in the UnitOfWork's DbContext to the database.
        /// </summary>
        public virtual new async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
        {
            UpdateEntitiesOnAddOrUpdate();
            Logger.Trace("Saving changes to database.");
            return await base.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
        }
        /// <summary>
        /// Transactions allow several database operations to be processed in an atomic manner.
        /// <para>Nested transaction is not allowed in Entity Framework Core. </para>
        /// <para>Usage: unitOfWork.BeginTransaction() </para>
        /// </summary>
        public void BeginTransaction()
        {
            if (this.Database.CurrentTransaction != null)
            {
                Logger.Error("There is a started Transaction detected, please use the other transaction instead of starting a new Transaction.");
                throw new MultipleTransactionException("There is a started Transaction detected, please use the other transaction instead of starting a new Transaction.");
            }
            this.Database.BeginTransaction();
            Logger.Trace("Transaction has began.");
        }
        /// <summary>
        /// Synchronously, save all the changes done in the transaction & commit the transaction into the database
        /// <para>Nested transaction is not allowed in Entity Framework Core. </para>
        /// <para>Usage: unitOfWork.CommitTransaction() </para>
        /// </summary>
        public void CommitTransaction()
        {
            if (this.Database.CurrentTransaction == null)
            {
                Logger.Error("Transaction does not exists. Please create one with UnitOfWork.BeginTransaction()");
                throw new MultipleTransactionException("Transaction does not exists. Please create one with UnitOfWork.BeginTransaction()");
            }
            SaveChanges();
            this.Database.CommitTransaction();
            Logger.Trace("Transaction is committed.");
        }
        /// <summary>
        /// Asynchronously, save all the changes done in the transaction & commit the transaction into the database
        /// <para>Nested transaction is not allowed in Entity Framework Core. </para>
        /// <para>Usage: unitOfWork.CommitTransaction() </para>
        /// </summary>
        public async Task CommitTransactionAsync(CancellationToken cancellationToken = default)
        {
            if (this.Database.CurrentTransaction == null)
            {
                Logger.Error("Transaction does not exists. Please create one with UnitOfWork.BeginTransaction()");
                throw new MultipleTransactionException("Transaction does not exists. Please create one with UnitOfWork.BeginTransaction()");
            }
            await SaveChangesAsync(cancellationToken).ConfigureAwait(false);
            this.Database.CommitTransaction();
            Logger.Trace("Transaction is committed.");
        }
        /// <summary>
        /// Rollback all of the changes done in the Transaction
        /// <para>Nested transaction is not allowed in Entity Framework Core. </para>
        /// <para>Usage: unitOfWork.RollbackTransaction() </para>
        /// </summary>
        public void RollbackTransaction()
        {
            if (this.Database.CurrentTransaction == null)
            {
                Logger.Error("Transaction does not exists. Please create one with UnitOfWork.BeginTransaction()");
                throw new MultipleTransactionException("Transaction does not exists. Please create one with UnitOfWork.BeginTransaction()");
            }
            this.Database.RollbackTransaction();
            Logger.Trace("Transaction has rolled back.");
        }
        /// <summary>
        /// Sets default connection string from appsettings.json.
        /// <para><paramref name="fromDI"/> indicates if the context is injected from dependency injection, sets to UseOracle/UseSqlserver from appsettings.json when it is not configured.</para>
        /// </summary>
        private void SetDefaultConnectionString(bool usingOracle = true, bool fromDI = false)
        {
            var config = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json", false)
                .Build();
            this.connectionString = config.GetConnectionString("DefaultConnection");
            if (fromDI)
            {
                if (config["UseOracle"] != null)
                {
                    this.useOracle = bool.Parse(config["UseOracle"]);
                }
                else
                {
                    this.useOracle = true;
                }
            }
            else
            {
                this.useOracle = usingOracle;
            }
        }
    }
}
